package lion.framework.core.web.views;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;

import lion.dev.io.MimeTypeUtil;
import lion.dev.lang.Lang;
import lion.framework.core.web.anno.ViewType;
import lion.framework.core.web.exception.WebException;

/**
 * File System Resource 系统文件下载视图，支持分断下载文件
 *
 * 传入参数
 *
 *
 *
 * @author lion
 * @email hl_0074@sina.com
 * @date 2014年6月14日
 */
@ViewType("fsresource")
public class FSResourceView implements IView {

	private static final int BUFFER_SIZE = 10240;// 10KB
	private static final String VIEW_TYPE = "fsresource";
	private static final String MIME_BOUNDARY = "LION_WEB_MIME_BOUNDARY";
	protected String name;
	protected String path;

	public FSResourceView() {

	}

	public FSResourceView(String name, String path) {

		this.name = name;
		this.path = path;
	}

	@Override
	public void render(HttpServletRequest request, HttpServletResponse response) throws Exception {

		String path = Lang.nvl((String) request.getAttribute("path"), this.path);
		if (path == null) { return; }
		try {
			sendfile(request, response, new File(path));
		} catch (WebException e) {
			throw e;
		} catch (Throwable e) {
			throw new WebException(500, e.getMessage());
		}
	}

	protected void sendfile(HttpServletRequest request, HttpServletResponse response, File file) throws Exception {

		if (!file.exists()
				|| file.isDirectory()) { throw new WebException(404, "File Not Found " + request.getRequestURI()); }
		if (!file.canRead()
				|| file.length() == 0) { throw new WebException(401, "Access denied " + request.getRequestURI()); }

		// ranges
		List<Range> ranges = null;
		try {
			ranges = parseRanges(request, response, file);
		} catch (Exception e) {
			response.addHeader("Content-Range", "bytes */" + file.length());
			throw new WebException(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE,
					"Requested Range Not Satisfiable");
		}

		response.setHeader("Accept-Ranges", "bytes");
		response.setBufferSize(BUFFER_SIZE);

		if (ranges == null || ranges.isEmpty()) {
			copy(response, file);
		} else {
			copyRanges(response, file, ranges);
		}
	}

	private List<Range> parseRanges(HttpServletRequest request, HttpServletResponse response, File file)
			throws Exception {

		String headerValue = request.getHeader("If-Range");
		if (headerValue != null && !StringUtils.equals(request.getHeader("ETag"), headerValue)) { return null; }

		String rangeHeader = request.getHeader("Range");
		if (rangeHeader == null) { return null; }

		if (!rangeHeader.startsWith("bytes")) { throw new WebException(
				HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, "Requested Range Not Satisfiable"); }

		rangeHeader = rangeHeader.substring(6);
		ArrayList<Range> result = new ArrayList<>();
		for (String rangeDefinition : rangeHeader.split(",")) {

			int dashPos = rangeDefinition.indexOf('-');
			if (dashPos == -1) { throw new WebException(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE,
					"Requested Range Not Satisfiable"); }

			Range currentRange = null;

			if (dashPos == 0) {
				try {
					long offset = Long.parseLong(rangeDefinition);
					currentRange = new Range(file.length() + offset, file.length() - 1, file.length());
				} catch (Exception e) {
					response.addHeader("Content-Range", "bytes */" + file.length());
					throw new WebException(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE,
							"Requested Range Not Satisfiable");
				}
			} else {

				long start = Long.parseLong(rangeDefinition.substring(0, dashPos));
				long end = file.length() - 1;
				if (dashPos < rangeDefinition.length() - 1) {
					end = Long.parseLong(rangeDefinition.substring(dashPos + 1));
				}
				currentRange = new Range(start, end, file.length());
			}

			if (currentRange.getEnd() >= file.length()) {
				currentRange.setEnd(file.length() - 1);
			}

			if (!currentRange.validate()) { throw new WebException(
					HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, "Requested Range Not Satisfiable"); }

			result.add(currentRange);
		}
		return result;
	}

	private void copyRanges(HttpServletResponse response, File file, List<Range> ranges) throws IOException {

		response.setContentType("multipart/byteranges; boundary=" + MIME_BOUNDARY);
		String mimeType = MimeTypeUtil.getMimeType(file.getName());

		ServletOutputStream ostream = response.getOutputStream();

		response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

		if (ranges.size() == 1) {
			Range range = ranges.get(0);
			long length = range.getLength();
			response.addHeader("Content-Range",
					"bytes " + range.getStart() + "-" + range.getEnd() + "/" + range.getSize());
			if (length < Integer.MAX_VALUE) {
				response.setContentLength((int) length);
			} else {
				response.setHeader("content-length", "" + length);
			}
			response.setContentType(mimeType);
			copyStream(response, new FileInputStream(file), range.getStart(), range.getLength());
		} else if (file.length() < 102400) {// 10k
			byte[] content = FileUtils.readFileToByteArray(file);
			for (Range range : ranges) {
				copyRange(response, mimeType, ostream, new ByteArrayInputStream(content), range);
			}
			ostream.println();
			ostream.print("--" + MIME_BOUNDARY + "--");
		} else {
			for (Range range : ranges) {
				copyRange(response, mimeType, ostream, new FileInputStream(file), range);
			}
			ostream.println();
			ostream.print("--" + MIME_BOUNDARY + "--");
		}
	}

	private void copyRange(HttpServletResponse response, String mimeType, ServletOutputStream ostream, InputStream ins,
			Range range) throws IOException {

		if (range.getLength() <= 0) { return; }
		ostream.println();
		ostream.println("--" + MIME_BOUNDARY);
		ostream.println("Content-Type: " + mimeType);
		ostream.println("Content-Range: bytes " + range.getStart() + "-" + range.getEnd() + "/" + range.getSize());
		ostream.println();
		copyStream(response, ins, range.getStart(), range.getLength());
	}

	private void copyStream(HttpServletResponse response, InputStream ins, long start, long len) throws IOException {

		try {
			IOUtils.copyLarge(ins, response.getOutputStream(), start, len);
		} finally {
			if (ins != null) {
				ins.close();
			}
		}
	}

	private void copy(HttpServletResponse response, File file) throws FileNotFoundException, IOException {

		response.setContentType(MimeTypeUtil.getMimeType(file.getName()));
		if (file.length() < Integer.MAX_VALUE) {
			response.setContentLength((int) file.length());
		} else {
			response.setHeader("content-length", "" + file.length());
		}
		copyStream(response, new FileInputStream(file), 0, file.length());
	}

	@Override
	public String getName() {

		return this.name;
	}

	@Override
	public String type() {

		return VIEW_TYPE;
	}

	class Range {

		private long start;
		private long end;
		private long size;

		public Range(long start, long end, long size) {

			this.start = start;
			this.end = end;
			this.size = size;
		}

		public long getSize() {

			return size;
		}

		public void setSize(long size) {

			this.size = size;
		}

		public long getStart() {

			return start;
		}

		public void setStart(long start) {

			this.start = start;
		}

		public long getEnd() {

			return end;
		}

		public void setEnd(long end) {

			this.end = end;
		}

		public long getLength() {

			return end - start + 1;
		}

		public boolean validate() {

			return start >= 0 && end >= 0 && start <= end;
		}

	}
}
